Android 类加载源码分析(一)

概述

本篇将对Android的类加载机制进行分析。总体来说Android的ClassLoader分为系统ClassLoader和自定义的ClassLoader
系统的包括有三种:

  1. BootClassLoader Android系统启动时会使用BootClassLoader预加载一些类。它位于类加载器链的头部。
  2. PathClassLoader 可以加载已经安装的apk,即也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。
  3. DexClassLoader,可以加载一个未安装的apk文件。

我们在App中使用的系统类加载器默认是PathClassLoader,它的父加载器是BootClassLoader。为什么是PathClassLoader呢?因为在contextImpl的getClassLoader有如下实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

// /frameworks/base/core/java/android/app/ContextImpl.java
@Override
public ClassLoader getClassLoader() {
return mPackageInfo != null ?
mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}

// /frameworks/base/core/java/android/app/LoadedApk.java
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}
……
if (mIncludeCode && !mPackageName.equals("android")) {
……
//创建PathClassLoader
mClassLoader =
ApplicationLoaders.getDefault().getClassLoader(
zip, libraryPath, mBaseClassLoader);
……

}
}
}

// /frameworks/base/core/java/android/app/ApplicationLoaders.java
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{
ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}
if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}
……
//创建PathClassLoader
PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent);

mLoaders.put(zip, pathClassloader);
return pathClassloader;
}

PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
……
return pathClassloader;
}
}

从上面可以看出ContextImpl为用户提供了PathClassLoader来供App加载。

BaseDexClassLoader

Android中使用PathClassLoader来加载类,其实实现的就是从本地文件系统中加载类,它继承自BaseDexClassLoader,BaseDexClassLoader继承子ClassLoader,ClassLoader是一个抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// libcore/libdvm/src/main/java/java/lang/ClassLoader.java
public abstract class ClassLoader {

static private class SystemClassLoader {
//实际上为PathClassLoader
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
...
/**
* The parent ClassLoader.
*/
private ClassLoader parent;

private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
//创建Android系统的ClassLoader即PathClassLoader
return new PathClassLoader(classPath, BootClassLoader.getInstance());
}
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
……
//返回已经被VM加载的类,如果已经加载过返回这个Class
protected final Class<?> findLoadedClass(String className) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, className);
}

//class加载的逻辑,先判断是否已经加载,已经加载就直接返回,否则通过findClass进行加载
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);//查找是否已经加载过该类

if (clazz == null) {//clazz为null表示未加载过
try {
clazz = parent.loadClass(className, false);//先通过parent加载,这也是遵循双亲委派模型
} catch (ClassNotFoundException e) {
// Don't want to see this.
}

if (clazz == null) {//parent loader未能加载则通过findClass来加载
clazz = findClass(className);//注意findClass在该类的实现为空,它是由子类实现类加载的逻辑的
}
}

return clazz;//返回加载的类
}

protected Class<?> findClass(String className) throws ClassNotFoundException {
throw new ClassNotFoundException(className);
}
}

可以看出ClassLoader为我们实现了类加载的基本逻辑,首先它通过findLoadedClass查找要加载的类是否已经加载,如果已经加载了就直接返回,否则通过parent loader进行加载,这符合双亲委派的加载模型,如果父类loader找到该类并加载则返回,否则通过子类加载器进行加载,子类加载时通过findClass进行加载的。所以需要实现findClass的具体逻辑。那么在BaseDexClassLoader我们需要重点关注findClass的类加载逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//BaseDexClassLoader继承自ClassLoader,这里将findClass的任务委托给了DexPathList
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {//没找到就抛出异常
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
……
}

我们发现BaseDexClassLoader的实现非常简单,它内部有一个DexPathList成员pathList,在构造方法中进行初始化,它代表了一个jar/apk文件列表,在这些文件之中包含了class文件和资源文件。而在findClass中实际上是将加载类的任务委托给了pathList。那么就需要再取分析DexPathList了,在这之前我们看看它大概会包含的信息,这是我在应用中打印的ClassLoader信息,它内部的DexPathList包含了该apk和lib的信息。

classLoader: dalvik.system.PathClassLoader[DexPathList[[zip file “/data/app/com.yujian.myapplication-2.apk”],nativeLibraryDirectories=[/data/app-lib/com.yujian.myapplication-2, /system/lib]]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
final class DexPathList {
...
private final Element[] dexElements;//Element是对应于dex/apk文件或者目录
……
private final File[] nativeLibraryDirectories;


//这里 dexPath可以包含多个dex文件,这也是为什么会叫DexPathList
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
……
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);//生成Element数组
……
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
}

DexPathList有两个成员dexElements和nativeLibraryDirectories,分别用来描述dex/apk文件信息和lib文件。它们都是在DexPathList构造方法中进行初始化的。其中dexElements是通过makeDexElements和splitLibraryPath生成的。其中Element的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 static class Element {
private final File file;//代表了源文件
private final boolean isDirectory;//当前描述的文件是否为目录
private final File zip;//源文件如果未压缩文件 zip和file就是同一个File
private final DexFile dexFile;//dex文件
……
public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
this.file = file;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFile = dexFile;
}
……
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {//遍历文件
File zip = null;
DexFile dex = null;
String name = file.getName();//文件名

if (name.endsWith(DEX_SUFFIX)) {//后缀为.dex
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);//直接通过loadDexFile加载
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {//后缀为.apk/.jar/.zip的情况
zip = file;//赋值给zip

try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if the
* zip file turns out to be resource-only (that is, no classes.dex file in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {//为目录的情况
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));//添加一个Element目录 true表示为目录
} else {
System.logW("Unknown file type for: " + file);
}

if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));//添加一个dex文件对应的Element
}
}

return elements.toArray(new Element[elements.size()]);
}

makeDexElements通过dexPath代表的dex/apk文件或者目录生成对应的elements数组,在构造DexPathList时传递的dexPath时可能包含多个文件路径的,我们上面打印的信息就只有一个apk,这里需要注意,这些文件路径经过splitDexPath返回一个ArrayList代表了dexPath所代表的文件列表。通过一个循环处理这个文件列表,针对不同的文件类型比如dex/apk/jar/zip或者目录进行不同的处理:

  1. 如果时dex文件,通过loadDexFile加载,并返回一个描述该文件的DexFile,
  2. 如果时zip/apk/jar,先保存压缩文件到zip中,然后通过loadDexFile加载并返回要描述的dex文件的DexFile
  3. 如果时目录,直接为其生成一个Element并添加到elements数组中

对于1和2两种情况最终也会未其分别创建Element并添加到elements数组中,这个elements数组最终就是我们要的dexElements。

1
2
3
4
5
6
7
8
9
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {//对于PathClassLoader来说 optimizedDirectory总是为null的
return new DexFile(file);//直接new一个DexFile返回
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}

loadDexFile实际上只是为file创建一个DexFile对象。从名称上看它是专门处理dex文件的。

下面我们就看看DexPathList是如何加载类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//遍历dexElements
DexFile dex = element.dexFile;

if (dex != null) {//通过dexFile加载类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;//加载成功
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

DexPathList先遍历dexElements,对于DexFile通过loadClassBinaryName来加载,如果找到就返回。这里又转到DexFile进行加载了,所以需要再看看DexFile是如何加载的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}

//DexFile加载类
private static Class defineClass(String name, ClassLoader loader, int cookie,
List<Throwable> suppressed) {
Class result = null;
try {
//通过native来加载 runtime/native/dalvik_system_DexFile.cc
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

DexFile调用defainClassNative方法来加载类,这里从名称看,它实际上是从native层加载类的,实现在runtime/native/dalvik_system_DexFile.cc。关于native层如何加载类我们在另外的篇章中进行分析。这样DexPathList加载类的逻辑就分析完成了。

需要注意的是DexPathList遍历dexElements通过DexFile来进行的类加载的方式,为一些基于muti dex的热修复技术提供了可能,因为在dexElements数组中靠前的dex文件首先被访问到,这样被修复的类可以被优先加载。

PathClassLoader

PathClassLoader是BaseDexClassLoader的子类,它的类加载功能正是依赖于其父类。
我们看看它的实现

1
2
3
4
5
6
7
8
9
10
11
public class PathClassLoader extends BaseDexClassLoader {

public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}

PathClassLoader的实现非常简单,只是提供了两个不同的构造方法,这两个构造方法的区别在于是否提供了lib path,默认情况下的path就是system/lib/和data/app-lib/pakage-name

DexClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getDir(String, int)} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getDir("dex", 0);
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {

public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}

DexClassLoader的实现更加简单,只有一个构造方法,从注释也可以看出它可以从包含classes.dex文件的jar/apk文件中来加载类,而不需要jar/apk为已安装应用的一部分。因为它提供了一个optimizedDirectory参数,这个参数是一个应用私有且可写入的目录,用来保存dex经过优化后的类,需要注意的是为了防止注入,优化后的类是不能被保存在外置存储上的。在PathClassLoader中我们看到这个参数默认是null,也就是它会从默认的位置加载dex,这个位置就是/data/dalvik-cache,也就是已经安装的apk。这也是它们之间最大的区别了。

坚持原创技术分享,您的支持将鼓励我继续创作!